/*
 * Hibernate Search, full-text search for your domain model
 *
 * License: GNU Lesser General Public License (LGPL), version 2.1 or later
 * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
 */
package org.hibernate.search.engine.environment.classpath.spi;

import java.lang.invoke.MethodHandles;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

import org.hibernate.search.engine.logging.impl.Log;
import org.hibernate.search.util.common.SearchException;
import org.hibernate.search.util.common.logging.impl.LoggerFactory;
import org.hibernate.search.util.common.impl.StringHelper;
import org.hibernate.search.util.common.impl.Throwables;

/**
 * Utility class to load instances of other classes by using a fully qualified name,
 * or from a class type.
 * Uses reflection and throws SearchException(s) with proper descriptions of the error,
 * such as the target class is missing a proper constructor, is an interface, is not found...
 *
 * @author Sanne Grinovero
 * @author Hardy Ferentschik
 * @author Ales Justin
 */
public class ClassLoaderHelper {

	private static final Log log = LoggerFactory.make( Log.class, MethodHandles.lookup() );

	private ClassLoaderHelper() {
	}

	/**
	 * Creates an instance of a target class specified by the fully qualified class name using a {@link ClassLoader}
	 * as fallback when the class cannot be found in the context one.
	 *
	 * @param <T> matches the type of targetSuperType: defines the return type
	 * @param targetSuperType the return type of the function, the classNameToLoad will be checked
	 * to be assignable to this type.
	 * @param classNameToLoad a fully qualified class name, whose type is assignable to targetSuperType
	 * @param componentDescription a meaningful description of the role the instance will have,
	 * used to enrich error messages to describe the context of the error
	 * @param classResolver the {@link ClassResolver} to use to load classes
	 *
	 * @return a new instance of the type given by {@code classNameToLoad}
	 *
	 * @throws SearchException wrapping other error types with a proper error message for all kind of problems, like
	 * classNotFound, missing proper constructor, wrong type, security errors.
	 */
	public static <T> T instanceFromName(Class<T> targetSuperType,
			String classNameToLoad,
			String componentDescription,
			ClassResolver classResolver) {
		final Class<?> clazzDef = classForName( classNameToLoad, componentDescription, classResolver );
		return instanceFromClass( targetSuperType, clazzDef, componentDescription );
	}

	/**
	 * Creates an instance of target class
	 *
	 * @param <T> the type of targetSuperType: defines the return type
	 * @param targetSuperType the created instance will be checked to be assignable to this type
	 * @param classToLoad the class to be instantiated
	 * @param componentDescription a role name/description to contextualize error messages
	 *
	 * @return a new instance of classToLoad
	 *
	 * @throws SearchException wrapping other error types with a proper error message for all kind of problems, like
	 * missing proper constructor, wrong type, securitymanager errors.
	 */
	public static <T> T instanceFromClass(Class<T> targetSuperType, Class<?> classToLoad, String componentDescription) {
		checkClassType( classToLoad, componentDescription );
		final Object instance = untypedInstanceFromClass( classToLoad, componentDescription );
		return verifySuperTypeCompatibility( targetSuperType, instance, classToLoad, componentDescription );
	}

	/**
	 * Creates an instance of target class. Similar to {@link #instanceFromClass(Class, Class, String)} but not checking
	 * the created instance will be of any specific type: using {@link #instanceFromClass(Class, Class, String)} should
	 * be preferred whenever possible.
	 *
	 * @param <T> the type of targetSuperType: defines the return type
	 * @param classToLoad the class to be instantiated
	 * @param componentDescription a role name/description to contextualize error messages. Ideally should be provided, but it can handle null.
	 *
	 * @return a new instance of classToLoad
	 *
	 * @throws SearchException wrapping other error types with a proper error message for all kind of problems, like
	 * missing proper constructor, securitymanager errors.
	 */
	public static <T> T untypedInstanceFromClass(final Class<T> classToLoad, final String componentDescription) {
		checkClassType( classToLoad, componentDescription );
		Constructor<T> constructor = noArgConstructor( classToLoad, componentDescription );
		try {
			return constructor.newInstance();
		}
		catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {
			if ( StringHelper.isEmpty( componentDescription ) ) {
				throw log.unableToInstantiateClass( classToLoad, Throwables.getFirstNonNullMessage( e ), e );
			}
			else {
				throw log.unableToInstantiateComponent(
						componentDescription, classToLoad, Throwables.getFirstNonNullMessage( e ), e
				);
			}
		}
	}

	/**
	 * Verifies that an object instance is implementing a specific interface, or extending a type.
	 *
	 * @param targetSuperType the type to extend, or the interface it should implement
	 * @param instance the object instance to be verified
	 * @param classToLoad the Class of the instance
	 * @param componentDescription a user friendly description of the component represented by the verified instance
	 *
	 * @return the same instance
	 */
	@SuppressWarnings("unchecked")
	private static <T> T verifySuperTypeCompatibility(Class<T> targetSuperType, Object instance, Class<?> classToLoad, String componentDescription) {
		if ( !targetSuperType.isInstance( instance ) ) {
			// have a proper error message according to interface implementation or subclassing
			if ( targetSuperType.isInterface() ) {
				throw log.interfaceImplementedExpected( componentDescription, classToLoad, targetSuperType );
			}
			else {
				throw log.subtypeExpected( componentDescription, classToLoad, targetSuperType );
			}
		}
		else {
			return (T) instance;
		}
	}

	/**
	 * Creates an instance of target class having a Map of strings as constructor parameter.
	 * Most of the Analyzer SPIs provided by Lucene have such a constructor.
	 *
	 * @param <T> the type of targetSuperType: defines the return type
	 * @param targetSuperType the created instance will be checked to be assignable to this type
	 * @param classToLoad the class to be instantiated
	 * @param componentDescription a role name/description to contextualize error messages
	 * @param constructorParameter a Map to be passed to the constructor. The loaded type must have such a constructor.
	 *
	 * @return a new instance of classToLoad
	 *
	 * @throws SearchException wrapping other error types with a proper error message for all kind of problems, like
	 * missing proper constructor, wrong type, security errors.
	 */
	public static <T> T instanceFromClass(Class<T> targetSuperType, Class<?> classToLoad, String componentDescription,
			Map<String, String> constructorParameter) {
		checkClassType( classToLoad, componentDescription );
		Constructor<?> singleMapConstructor = singleMapConstructor( classToLoad, componentDescription );
		if ( constructorParameter == null ) {
			constructorParameter = new HashMap<>( 0 );//can't use the emptyMap singleton as it needs to be mutable
		}
		final Object instance;
		try {
			instance = singleMapConstructor.newInstance( constructorParameter );
		}
		catch (Exception e) {
			throw log.unableToInstantiateComponent(
					componentDescription, classToLoad, Throwables.getFirstNonNullMessage( e ), e
			);
		}
		return verifySuperTypeCompatibility( targetSuperType, instance, classToLoad, componentDescription );
	}

	private static void checkClassType(Class<?> classToLoad, String componentDescription) {
		if ( classToLoad.isInterface() ) {
			throw log.implementationRequired( componentDescription, classToLoad );
		}
	}

	/**
	 * Verifies if target class has a no-args constructor, and that it is
	 * accessible in current security manager.
	 * If checks are succesfull, return the constructor; otherwise appropriate exceptions are thrown.
	 * @param classToLoad the class type to check
	 * @param componentDescription adds a meaningful description to the type to describe in the error messsage
	 */
	private static <T> Constructor<T> noArgConstructor(Class<T> classToLoad, String componentDescription) {
		try {
			return classToLoad.getConstructor();
		}
		catch (SecurityException e) {
			throw log.securityManagerLoadingError( componentDescription, classToLoad, e );
		}
		catch (NoSuchMethodException e) {
			throw log.noPublicNoArgConstructor( componentDescription, classToLoad );
		}
	}

	private static Constructor<?> singleMapConstructor(Class<?> classToLoad, String componentDescription) {
		try {
			return classToLoad.getConstructor( Map.class );
		}
		catch (SecurityException e) {
			throw log.securityManagerLoadingError( componentDescription, classToLoad, e );
		}
		catch (NoSuchMethodException e) {
			throw log.missingConstructor( componentDescription, classToLoad );
		}
	}

	public static Class<?> classForName(String classNameToLoad, String componentDescription, ClassResolver classResolver) {
		Class<?> clazz;
		try {
			clazz = classResolver.classForName( classNameToLoad );
		}
		catch (ClassLoadingException e) {
			throw log.unableToFindComponentImplementation( componentDescription, classNameToLoad, e );
		}
		return clazz;
	}

	public static <T> Class<? extends T> classForName(Class<T> targetSuperType,
			String classNameToLoad,
			String componentDescription,
			ClassResolver classResolver) {
		final Class<?> clazzDef = classForName( classNameToLoad, componentDescription, classResolver );
		try {
			return clazzDef.asSubclass( targetSuperType );
		}
		catch (ClassCastException cce) {
			throw log.notAssignableImplementation( componentDescription, classNameToLoad, targetSuperType );
		}
	}

	/**
	 * Perform resolution of a class name.
	 * <p>
	 * Here we first check the context classloader, if one, before delegating to
	 * {@link Class#forName(String, boolean, ClassLoader)} using the caller's classloader
	 *
	 * @param classNameToLoad The class name
	 * @param classResolver The {@link ClassResolver} to use to load classes
	 *
	 * @return The class reference.
	 *
	 * @throws ClassLoadingException From {@link Class#forName(String, boolean, ClassLoader)}.
	 */
	public static Class<?> classForName(String classNameToLoad, ClassResolver classResolver) {
		return classResolver.classForName( classNameToLoad );
	}
}
